1
|
|
|
/* |
2
|
|
|
backgrid-paginator |
3
|
|
|
http://github.com/wyuenho/backgrid |
4
|
|
|
|
5
|
|
|
Copyright (c) 2013-present Cloudflare, Inc and contributors |
6
|
|
|
Licensed under the MIT @license. |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
import _ from 'underscore'; |
|
|
|
|
10
|
|
|
import Backbone from 'backbone'; |
11
|
|
|
|
12
|
|
|
import { |
13
|
|
|
Backgrid |
14
|
|
|
} from '../backgrid.es6.js'; |
15
|
|
|
|
16
|
|
|
"use strict"; |
|
|
|
|
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
PageHandle is a class that renders the actual page handles and reacts to |
20
|
|
|
click events for pagination. |
21
|
|
|
|
22
|
|
|
This class acts in two modes - control or discrete page handle modes. If |
23
|
|
|
one of the `is*` flags is `true`, an instance of this class is under |
24
|
|
|
control page handle mode. Setting a `pageIndex` to an instance of this |
25
|
|
|
class under control mode has no effect and the correct page index will |
26
|
|
|
always be inferred from the `is*` flag. Only one of the `is*` flags should |
27
|
|
|
be set to `true` at a time. For example, an instance of this class cannot |
28
|
|
|
simultaneously be a rewind control and a fast forward control. A `label` |
29
|
|
|
and a `title` function or a string are required to be passed to the |
30
|
|
|
constuctor under this mode. If a `title` function is provided, it __MUST__ |
31
|
|
|
accept a hash parameter `data`, which contains a key `label`. Its result |
32
|
|
|
will be used to render the generated anchor's title attribute. |
33
|
|
|
|
34
|
|
|
If all of the `is*` flags is set to `false`, which is the default, an |
35
|
|
|
instance of this class will be in discrete page handle mode. An instance |
36
|
|
|
under this mode requires the `pageIndex` to be passed from the constructor |
37
|
|
|
as an option and it __MUST__ be a 0-based index of the list of page numbers |
38
|
|
|
to render. The constuctor will normalize the base to the same base the |
39
|
|
|
underlying PageableCollection collection instance uses. A `label` is not |
40
|
|
|
required under this mode, which will default to the equivalent 1-based page |
41
|
|
|
index calculated from `pageIndex` and the underlying PageableCollection |
42
|
|
|
instance. A provided `label` will still be honored however. The `title` |
43
|
|
|
parameter is also not required under this mode, in which case the default |
44
|
|
|
`title` function will be used. You are encouraged to provide your own |
45
|
|
|
`title` function however if you wish to localize the title strings. |
46
|
|
|
|
47
|
|
|
If this page handle represents the current page, an `active` class will be |
48
|
|
|
placed on the root list element. |
49
|
|
|
|
50
|
|
|
If this page handle is at the border of the list of pages, a `disabled` |
51
|
|
|
class will be placed on the root list element. |
52
|
|
|
|
53
|
|
|
Only page handles that are neither `active` nor `disabled` will respond to |
54
|
|
|
click events and triggers pagination. |
55
|
|
|
|
56
|
|
|
@class Backgrid.Extension.PageHandle |
57
|
|
|
*/ |
58
|
|
|
var PageHandle = Backgrid.Extension.PageHandle = Backbone.View.extend({ |
59
|
|
|
|
60
|
|
|
/** @property */ |
61
|
|
|
tagName: "li", |
62
|
|
|
|
63
|
|
|
/** @property */ |
64
|
|
|
events: { |
65
|
|
|
"click a": "changePage" |
66
|
|
|
}, |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
@property {string|function(Object.<string, string>): string} title |
70
|
|
|
The title to use for the `title` attribute of the generated page handle |
71
|
|
|
anchor elements. It can be a string or a function that takes a `data` |
72
|
|
|
parameter, which contains a mandatory `label` key which provides the |
73
|
|
|
label value to be displayed. |
74
|
|
|
*/ |
75
|
|
|
title: function (data) { |
76
|
|
|
return 'Page ' + data.label; |
77
|
|
|
}, |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
@property {boolean} isRewind Whether this handle represents a rewind |
81
|
|
|
control |
82
|
|
|
*/ |
83
|
|
|
isRewind: false, |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
@property {boolean} isBack Whether this handle represents a back |
87
|
|
|
control |
88
|
|
|
*/ |
89
|
|
|
isBack: false, |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
@property {boolean} isForward Whether this handle represents a forward |
93
|
|
|
control |
94
|
|
|
*/ |
95
|
|
|
isForward: false, |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
@property {boolean} isFastForward Whether this handle represents a fast |
99
|
|
|
forward control |
100
|
|
|
*/ |
101
|
|
|
isFastForward: false, |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
Initializer. |
105
|
|
|
|
106
|
|
|
@param {Object} options |
107
|
|
|
@param {Backbone.Collection} options.collection |
108
|
|
|
@param {number} pageIndex 0-based index of the page number this handle |
|
|
|
|
109
|
|
|
handles. This parameter will be normalized to the base the underlying |
110
|
|
|
PageableCollection uses. |
111
|
|
|
@param {string} [options.label] If provided it is used to render the |
112
|
|
|
anchor text, otherwise the normalized pageIndex will be used |
113
|
|
|
instead. Required if any of the `is*` flags is set to `true`. |
114
|
|
|
@param {string} [options.title] |
115
|
|
|
@param {boolean} [options.isRewind=false] |
116
|
|
|
@param {boolean} [options.isBack=false] |
117
|
|
|
@param {boolean} [options.isForward=false] |
118
|
|
|
@param {boolean} [options.isFastForward=false] |
119
|
|
|
*/ |
120
|
|
|
initialize: function (options) { |
121
|
|
|
var collection = this.collection; |
122
|
|
|
var state = collection.state; |
123
|
|
|
var currentPage = state.currentPage; |
124
|
|
|
var firstPage = state.firstPage; |
125
|
|
|
var lastPage = state.lastPage; |
126
|
|
|
|
127
|
|
|
_.extend(this, _.pick(options, ["isRewind", "isBack", "isForward", "isFastForward"])); |
128
|
|
|
|
129
|
|
|
var pageIndex; |
130
|
|
|
if (this.isRewind) pageIndex = firstPage; |
|
|
|
|
131
|
|
|
else if (this.isBack) pageIndex = Math.max(firstPage, currentPage - 1); |
|
|
|
|
132
|
|
|
else if (this.isForward) pageIndex = Math.min(lastPage, currentPage + 1); |
|
|
|
|
133
|
|
|
else if (this.isFastForward) pageIndex = lastPage; |
|
|
|
|
134
|
|
|
else { |
135
|
|
|
pageIndex = +options.pageIndex; |
136
|
|
|
pageIndex = (firstPage ? pageIndex + 1 : pageIndex); |
137
|
|
|
} |
138
|
|
|
this.pageIndex = pageIndex; |
139
|
|
|
|
140
|
|
|
this.label = (options.label || (firstPage ? pageIndex : pageIndex + 1)) + ''; |
141
|
|
|
var title = options.title || this.title; |
142
|
|
|
this.title = _.isFunction(title) ? title({ |
143
|
|
|
label: this.label |
144
|
|
|
}) : title; |
145
|
|
|
}, |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
Renders a clickable anchor element under a list item. |
149
|
|
|
*/ |
150
|
|
|
render: function () { |
151
|
|
|
this.$el.empty(); |
152
|
|
|
var anchor = document.createElement("a"); |
153
|
|
|
anchor.href = '#'; |
154
|
|
|
if (this.title) anchor.title = this.title; |
|
|
|
|
155
|
|
|
anchor.innerHTML = this.label; |
156
|
|
|
this.el.appendChild(anchor); |
157
|
|
|
|
158
|
|
|
var collection = this.collection; |
159
|
|
|
var state = collection.state; |
160
|
|
|
var currentPage = state.currentPage; |
161
|
|
|
var pageIndex = this.pageIndex; |
162
|
|
|
|
163
|
|
|
if (this.isRewind && currentPage == state.firstPage || |
|
|
|
|
164
|
|
|
this.isBack && !collection.hasPreviousPage() || |
165
|
|
|
this.isForward && !collection.hasNextPage() || |
166
|
|
|
this.isFastForward && (currentPage == state.lastPage || state.totalPages < 1)) { |
|
|
|
|
167
|
|
|
this.$el.addClass("disabled"); |
168
|
|
|
} else if (!(this.isRewind || |
169
|
|
|
this.isBack || |
170
|
|
|
this.isForward || |
171
|
|
|
this.isFastForward) && |
172
|
|
|
state.currentPage == pageIndex) { |
|
|
|
|
173
|
|
|
this.$el.addClass("active"); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
this.delegateEvents(); |
177
|
|
|
return this; |
178
|
|
|
}, |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
jQuery click event handler. Goes to the page this PageHandle instance |
182
|
|
|
represents. No-op if this page handle is currently active or disabled. |
183
|
|
|
*/ |
184
|
|
|
changePage: function (e) { |
185
|
|
|
e.preventDefault(); |
186
|
|
|
var $el = this.$el, |
187
|
|
|
col = this.collection; |
188
|
|
|
if (!$el.hasClass("active") && !$el.hasClass("disabled")) { |
189
|
|
|
if (this.isRewind) col.getFirstPage({ |
|
|
|
|
190
|
|
|
reset: true |
191
|
|
|
}); |
192
|
|
|
else if (this.isBack) col.getPreviousPage({ |
|
|
|
|
193
|
|
|
reset: true |
194
|
|
|
}); |
195
|
|
|
else if (this.isForward) col.getNextPage({ |
|
|
|
|
196
|
|
|
reset: true |
197
|
|
|
}); |
198
|
|
|
else if (this.isFastForward) col.getLastPage({ |
|
|
|
|
199
|
|
|
reset: true |
200
|
|
|
}); |
201
|
|
|
else col.getPage(this.pageIndex, { |
|
|
|
|
202
|
|
|
reset: true |
203
|
|
|
}); |
204
|
|
|
} |
205
|
|
|
return this; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
}); |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
Paginator is a Backgrid extension that renders a series of configurable |
212
|
|
|
pagination handles. This extension is best used for splitting a large data |
213
|
|
|
set across multiple pages. If the number of pages is larger then a |
214
|
|
|
threshold, which is set to 10 by default, the page handles are rendered |
215
|
|
|
within a sliding window, plus the rewind, back, forward and fast forward |
216
|
|
|
control handles. The individual control handles can be turned off. |
217
|
|
|
|
218
|
|
|
@class Backgrid.Extension.Paginator |
219
|
|
|
*/ |
220
|
|
|
var Paginator = Backgrid.Extension.Paginator = Backbone.View.extend({ |
221
|
|
|
|
222
|
|
|
/** @property */ |
223
|
|
|
className: "backgrid-paginator", |
224
|
|
|
|
225
|
|
|
/** @property */ |
226
|
|
|
windowSize: 10, |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
@property {number} slideScale the number used by #slideHowMuch to scale |
230
|
|
|
`windowSize` to yield the number of pages to slide. For example, the |
231
|
|
|
default windowSize(10) * slideScale(0.5) yields 5, which means the window |
232
|
|
|
will slide forward 5 pages as soon as you've reached page 6. The smaller |
233
|
|
|
the scale factor the less pages to slide, and vice versa. |
234
|
|
|
|
235
|
|
|
Also See: |
236
|
|
|
|
237
|
|
|
- #slideMaybe |
238
|
|
|
- #slideHowMuch |
239
|
|
|
*/ |
240
|
|
|
slideScale: 0.5, |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
@property {Object.<string, Object.<string, string>>} controls You can |
244
|
|
|
disable specific control handles by setting the keys in question to |
245
|
|
|
null. The defaults will be merged with your controls object, with your |
246
|
|
|
changes taking precedent. |
247
|
|
|
*/ |
248
|
|
|
controls: { |
249
|
|
|
rewind: { |
250
|
|
|
label: "《", |
251
|
|
|
title: "First" |
252
|
|
|
}, |
253
|
|
|
back: { |
254
|
|
|
label: "〈", |
255
|
|
|
title: "Previous" |
256
|
|
|
}, |
257
|
|
|
forward: { |
258
|
|
|
label: "〉", |
259
|
|
|
title: "Next" |
260
|
|
|
}, |
261
|
|
|
fastForward: { |
262
|
|
|
label: "》", |
263
|
|
|
title: "Last" |
264
|
|
|
} |
265
|
|
|
}, |
266
|
|
|
|
267
|
|
|
/** @property */ |
268
|
|
|
renderIndexedPageHandles: true, |
269
|
|
|
|
270
|
|
|
/** |
271
|
|
|
@property renderMultiplePagesOnly. Determines if the paginator |
272
|
|
|
should show in cases where the collection has more than one page. |
273
|
|
|
Default is false for backwards compatibility. |
274
|
|
|
*/ |
275
|
|
|
renderMultiplePagesOnly: false, |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
@property {Backgrid.Extension.PageHandle} pageHandle. The PageHandle |
279
|
|
|
class to use for rendering individual handles |
280
|
|
|
*/ |
281
|
|
|
pageHandle: PageHandle, |
282
|
|
|
|
283
|
|
|
/** @property */ |
284
|
|
|
goBackFirstOnSort: true, |
285
|
|
|
|
286
|
|
|
/** |
287
|
|
|
Initializer. |
288
|
|
|
|
289
|
|
|
@param {Object} options |
290
|
|
|
@param {Backbone.Collection} options.collection |
291
|
|
|
@param {boolean} [options.controls] |
292
|
|
|
@param {boolean} [options.pageHandle=Backgrid.Extension.PageHandle] |
293
|
|
|
@param {boolean} [options.goBackFirstOnSort=true] |
294
|
|
|
@param {boolean} [options.renderMultiplePagesOnly=false] |
295
|
|
|
*/ |
296
|
|
|
initialize: function (options) { |
297
|
|
|
var self = this; |
|
|
|
|
298
|
|
|
self.controls = _.defaults(options.controls || {}, self.controls, |
299
|
|
|
Paginator.prototype.controls); |
300
|
|
|
|
301
|
|
|
_.extend(self, _.pick(options || {}, "windowSize", "pageHandle", |
302
|
|
|
"slideScale", "goBackFirstOnSort", |
303
|
|
|
"renderIndexedPageHandles", |
304
|
|
|
"renderMultiplePagesOnly")); |
305
|
|
|
|
306
|
|
|
var col = self.collection; |
307
|
|
|
self.listenTo(col, "add", self.render); |
308
|
|
|
self.listenTo(col, "remove", self.render); |
309
|
|
|
self.listenTo(col, "reset", self.render); |
310
|
|
|
self.listenTo(col, "backgrid:sorted", function () { |
311
|
|
|
if (self.goBackFirstOnSort && col.state.currentPage !== col.state.firstPage) col.getFirstPage({ |
|
|
|
|
312
|
|
|
reset: true |
313
|
|
|
}); |
314
|
|
|
}); |
315
|
|
|
}, |
316
|
|
|
|
317
|
|
|
/** |
318
|
|
|
Decides whether the window should slide. This method should return 1 if |
319
|
|
|
sliding should occur and 0 otherwise. The default is sliding should occur |
320
|
|
|
if half of the pages in a window has been reached. |
321
|
|
|
|
322
|
|
|
__Note__: All the parameters have been normalized to be 0-based. |
323
|
|
|
|
324
|
|
|
@param {number} firstPage |
325
|
|
|
@param {number} lastPage |
326
|
|
|
@param {number} currentPage |
327
|
|
|
@param {number} windowSize |
328
|
|
|
@param {number} slideScale |
329
|
|
|
|
330
|
|
|
@return {0|1} |
331
|
|
|
*/ |
332
|
|
|
slideMaybe: function (firstPage, lastPage, currentPage, windowSize, slideScale) { |
|
|
|
|
333
|
|
|
return Math.round(currentPage % windowSize / windowSize); |
334
|
|
|
}, |
335
|
|
|
|
336
|
|
|
/** |
337
|
|
|
Decides how many pages to slide when sliding should occur. The default |
338
|
|
|
simply scales the `windowSize` to arrive at a fraction of the `windowSize` |
339
|
|
|
to increment. |
340
|
|
|
|
341
|
|
|
__Note__: All the parameters have been normalized to be 0-based. |
342
|
|
|
|
343
|
|
|
@param {number} firstPage |
344
|
|
|
@param {number} lastPage |
345
|
|
|
@param {number} currentPage |
346
|
|
|
@param {number} windowSize |
347
|
|
|
@param {number} slideScale |
348
|
|
|
|
349
|
|
|
@return {number} |
350
|
|
|
*/ |
351
|
|
|
slideThisMuch: function (firstPage, lastPage, currentPage, windowSize, slideScale) { |
352
|
|
|
return ~~(windowSize * slideScale); |
|
|
|
|
353
|
|
|
}, |
354
|
|
|
|
355
|
|
|
_calculateWindow: function () { |
356
|
|
|
var collection = this.collection; |
357
|
|
|
var state = collection.state; |
358
|
|
|
|
359
|
|
|
// convert all indices to 0-based here |
360
|
|
|
var firstPage = state.firstPage; |
361
|
|
|
var lastPage = +state.lastPage; |
362
|
|
|
lastPage = Math.max(0, firstPage ? lastPage - 1 : lastPage); |
363
|
|
|
var currentPage = Math.max(state.currentPage, state.firstPage); |
364
|
|
|
currentPage = firstPage ? currentPage - 1 : currentPage; |
365
|
|
|
var windowSize = this.windowSize; |
366
|
|
|
var slideScale = this.slideScale; |
367
|
|
|
var windowStart = Math.floor(currentPage / windowSize) * windowSize; |
368
|
|
|
if (currentPage <= lastPage - this.slideThisMuch()) { |
369
|
|
|
windowStart += (this.slideMaybe(firstPage, lastPage, currentPage, windowSize, slideScale) * |
370
|
|
|
this.slideThisMuch(firstPage, lastPage, currentPage, windowSize, slideScale)); |
371
|
|
|
} |
372
|
|
|
var windowEnd = Math.min(lastPage + 1, windowStart + windowSize); |
373
|
|
|
return [windowStart, windowEnd]; |
374
|
|
|
}, |
375
|
|
|
|
376
|
|
|
/** |
377
|
|
|
Creates a list of page handle objects for rendering. |
378
|
|
|
|
379
|
|
|
@return {Array.<Object>} an array of page handle objects hashes |
380
|
|
|
*/ |
381
|
|
|
makeHandles: function () { |
382
|
|
|
|
383
|
|
|
var handles = []; |
384
|
|
|
var collection = this.collection; |
385
|
|
|
|
386
|
|
|
var window = this._calculateWindow(); |
387
|
|
|
var winStart = window[0], |
388
|
|
|
winEnd = window[1]; |
389
|
|
|
|
390
|
|
|
if (this.renderIndexedPageHandles) { |
391
|
|
|
for (var i = winStart; i < winEnd; i++) { |
392
|
|
|
handles.push(new this.pageHandle({ |
|
|
|
|
393
|
|
|
collection: collection, |
394
|
|
|
pageIndex: i |
395
|
|
|
})); |
396
|
|
|
} |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
var controls = this.controls; |
400
|
|
|
_.each(["back", "rewind", "forward", "fastForward"], function (key) { |
401
|
|
|
var value = controls[key]; |
402
|
|
|
if (value) { |
403
|
|
|
var handleCtorOpts = { |
404
|
|
|
collection: collection, |
405
|
|
|
title: value.title, |
406
|
|
|
label: value.label |
407
|
|
|
}; |
408
|
|
|
handleCtorOpts["is" + key.slice(0, 1).toUpperCase() + key.slice(1)] = true; |
409
|
|
|
var handle = new this.pageHandle(handleCtorOpts); |
|
|
|
|
410
|
|
|
if (key == "rewind" || key == "back") handles.unshift(handle); |
|
|
|
|
411
|
|
|
else handles.push(handle); |
|
|
|
|
412
|
|
|
} |
413
|
|
|
}, this); |
414
|
|
|
|
415
|
|
|
return handles; |
416
|
|
|
}, |
417
|
|
|
|
418
|
|
|
/** |
419
|
|
|
Render the paginator handles inside an unordered list. |
420
|
|
|
*/ |
421
|
|
|
render: function () { |
422
|
|
|
this.$el.empty(); |
423
|
|
|
|
424
|
|
|
var totalPages = this.collection.state.totalPages; |
425
|
|
|
|
426
|
|
|
// Don't render if collection is empty |
427
|
|
|
if (this.renderMultiplePagesOnly && totalPages <= 1) { |
428
|
|
|
return this; |
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
if (this.handles) { |
432
|
|
|
for (var i = 0, l = this.handles.length; i < l; i++) { |
433
|
|
|
this.handles[i].remove(); |
434
|
|
|
} |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
var handles = this.handles = this.makeHandles(); |
438
|
|
|
|
439
|
|
|
var ul = document.createElement("ul"); |
440
|
|
|
for (var i = 0; i < handles.length; i++) { |
|
|
|
|
441
|
|
|
ul.appendChild(handles[i].render().el); |
442
|
|
|
} |
443
|
|
|
|
444
|
|
|
this.el.appendChild(ul); |
445
|
|
|
|
446
|
|
|
return this; |
447
|
|
|
} |
448
|
|
|
|
449
|
|
|
}); |
450
|
|
|
|
451
|
|
|
export { |
452
|
|
|
PageHandle, |
453
|
|
|
Paginator |
454
|
|
|
}; |
455
|
|
|
|